package cscalendar.classes;

import cscalendar.exceptions.CSWrongCommandFormatException;
import cscalendar.interfaces.CSCalendarElement;
import cscalendar.interfaces.CSStorable;
import java.awt.geom.Point2D;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Scanner;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * CSCalendar adalah kelas yang mewakili sebuah kalendar. CSCalendar memiliki
 * atribut events berupa priority queue yang menampung CSEvent terurut membesar
 * berdasarkan atribut date dari event. CSCalendar memiliki identitas berupa
 * entry number yang membedakan satu objek dengan objek lain.
 *
 * @author Muqtafi Akhmad
 */
public class CSCalendar implements CSCalendarElement, CSStorable {

    /**
     * mengisikan events
     *
     * @param events antrian events
     */
    public void setEvents(PriorityQueue<CSEvent> events) {
        this.events = events;
    }

    /**
     * mengisikan entry number
     *
     * @param entryNo entry number
     */
    public void setEntryNo(Integer entryNo) {
        this.entryNo = entryNo;
    }

    /**
     * mengembalikan antrian events
     *
     * @return events
     */
    public PriorityQueue<CSEvent> getEvents() {
        return events;
    }

    /**
     * mengembalikan entry number
     *
     * @return entryNo
     */
    public Integer getEntryNo() {
        return entryNo;
    }
    /**
     * nilai jumlah event default dari CSCalendar
     */
    private final static Integer DEFAULT_EVENT_COUNT = 100;
    /**
     * antrian event
     */
    private PriorityQueue<CSEvent> events;
    /**
     * entry number
     */
    private Integer entryNo;
    /**
     * antrian previous calendar dari current
     */
    Stack<CSCalendar> prevCalendar = new Stack<>();
    /**
     * antrian next calendar dari current
     */
    Stack<CSCalendar> afterCalendar = new Stack<>();

    /**
     * konstruktor default dari CSCalendar
     */
    public CSCalendar() {
        this.events = new PriorityQueue<>(DEFAULT_EVENT_COUNT, new CSEvent.EventComparator());
        this.entryNo = new Random(this.hashCode()).nextInt(10000000);
        System.out.println("Succesfully Create Calendar with ID : " + this.entryNo);
    }
    
    public CSCalendar(int entryNo){
        this.events = new PriorityQueue<>(DEFAULT_EVENT_COUNT, new CSEvent.EventComparator());
        this.entryNo = entryNo;
        System.out.println("Succesfully Create Calendar with ID : " + this.entryNo);
    }

    /**
     * konstruktor berparameter event count dan entry number
     *
     * @param eventCount jumlah event
     * @param entryNo entry number
     */
    public CSCalendar(Integer eventCount, Integer entryNo) {
        this.events = new PriorityQueue<>(eventCount, new CSEvent.EventComparator());
        this.entryNo = entryNo;
    }

    /**
     * konstruktor berparameter antrian events dan entry number
     *
     * @param events antrian events
     * @param entryNo entry number
     */
    public CSCalendar(PriorityQueue<CSEvent> events, Integer entryNo) {
        this.events = events;
        this.entryNo = entryNo;
    }

    /**
     * copy constructor dari CSCalendar
     *
     * @param otherCalendar calendar lain
     */
    public CSCalendar(CSCalendar otherCalendar) {
        this.events = new PriorityQueue<>(otherCalendar.events.size(), new CSEvent.EventComparator());
        // copy event
        for (CSEvent event : otherCalendar.events) {
            this.events.add((CSEvent) event.elementClone());
        }
        this.entryNo = otherCalendar.entryNo;
    }

    /**
     * mengembalikan reference CSEvent dengan entry number yang diberikan
     * mengembalikan null jika not exist
     *
     * @param eventEntryNo entry number dari event
     * @return reference ke event
     */
    public CSEvent eventExist(Integer eventEntryNo) {
        for (CSEvent event : events) {
            if (event.getEntryNo().equals(eventEntryNo)) {
                return event;
            }
        }
        return null;
    }

    /**
     * melihat sebuah event sesuai dengan entry number
     *
     * @param args entry number
     */
    public void executeViewEvent(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            Integer tempEntryNo = Integer.parseInt(args[2]);
            CSEvent eventRef = eventExist(tempEntryNo);
            if (eventRef == null) {
                System.out.println("Event with entry number : " + tempEntryNo + " not found");
            } else {
                System.out.println(eventRef.elementToString());
            }
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * load sebuah file calendar
     *
     * @param args file name
     */
    public void executeLoadCalendar(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            // load calendar
            this.loadFromFile(args[2]);
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * save calendar ke dalam file
     *
     * @param args file name to save events
     */
    public void executeSaveCalendar(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            // save calendar
            this.saveToFile(args[2]);
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * menambahkan event baru ke calendar
     *
     * @param args the new event entry number
     */
    public void executeAddEvent(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            Integer tempEntryNo = Integer.parseInt(args[2]);
            CSEvent eventRef = eventExist(tempEntryNo);
            if (eventRef != null) {
                System.out.println("Event with entry number " + tempEntryNo + " already exists \n");
            } else {
                try {
                    System.out.println("Please fill in the event details \n");
                    // read event description
                    System.out.println("Event description : \n\t");
                    String description;
                    BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
                    description = bufferRead.readLine();
                    // read event position
                    System.out.println("Event position (lat, long) : \n\t");
                    String buff;
                    String buffArr[];
                    buff = bufferRead.readLine();
                    buffArr = buff.split(",");
                    Point2D position;
                    position = new Point2D.Float(Float.parseFloat(buffArr[0]), Float.parseFloat(buffArr[1]));

                    // read date time
                    System.out.println("Event date and time (YYYY-MM-DD@hour:min:sec) : \n\t");
                    buff = bufferRead.readLine();
                    buffArr = buff.split("@");
                    String date_string = buffArr[0], time_string = buffArr[1];
                    buffArr = date_string.split("-");
                    Calendar date = Calendar.getInstance();
                    date.set(Calendar.YEAR, Integer.parseInt(buffArr[0]));
                    date.set(Calendar.MONTH, Integer.parseInt(buffArr[1]) - 1);
                    date.set(Calendar.DATE, Integer.parseInt(buffArr[2]));
                    buffArr = time_string.split(":");
                    date.set(Calendar.HOUR, Integer.parseInt(buffArr[0]));
                    date.set(Calendar.MINUTE, Integer.parseInt(buffArr[1]));
                    date.set(Calendar.SECOND, Integer.parseInt(buffArr[2]));

                    // read participants
                    System.out.println("Number of participants : \n\t");
                    int participantCount, iter;
                    ArrayList<String> participants = new ArrayList<>();
                    participantCount = Integer.parseInt(bufferRead.readLine());
                    for (iter = 0; iter < participantCount; iter++) {
                        System.out.print("        Participant #" + (iter + 1) + " : ");
                        buff = bufferRead.readLine();
                        participants.add(buff);
                    }

                    // insert event to event list
                    CSEvent event = new CSEvent(entryNo, description, position, date, participants);
                    events.add(event);
                    System.out.println("Success inserting new event with entry number " + tempEntryNo + " \n");
                } catch (IOException ex) {
                    //Logger.getLogger(CSCalendar.class.getName()).log(Level.SEVERE, null, ex);
                } catch (NumberFormatException ex) {
                    //Logger.getLogger(CSCalendar.class.getName()).log(Level.SEVERE, null, ex);
                    throw new CSWrongCommandFormatException("invalid input");
                }
            }
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * menghapus sebuah event dari calendar
     *
     * @param args the entry number of deleted event
     */
    public void executeDeleteEvent(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            Integer tempEntryNo = Integer.parseInt(args[2]);
            CSEvent tempEvent = eventExist(tempEntryNo);
            if (tempEvent == null) {
                System.out.println("Event with entry number " + tempEntryNo + " not found");
            } else {
                CSEvent tempEvent2 = new CSEvent();
                tempEvent2.setEntryNo(tempEntryNo);
                events.remove(tempEvent2);
            }
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * print sejumlah event di awal antrian
     *
     * @param args number of displayed event
     */
    public void executePrintFirst(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            int i = 0, count = Integer.parseInt(args[2]);
            for (CSEvent event : events) {
                i++;
                System.out.println(event.elementToString());
                if (i == count) {
                    break;
                }
            }
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * print sejumlah event di akhir antrian
     *
     * @param args number of displayed event
     */
    public void executePrintLast(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            int i = 0, start = events.size() - Integer.parseInt(args[2]);
            for (CSEvent event : events) {
                if (i >= start) {
                    System.out.println(event.elementToString());
                }
                i++;
            }
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * menampilkan event yang ada dalam tanggal tertentu
     *
     * @param args date input
     */
    public void executeShowDetail(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 3) {
            // parse date
            String buff = args[2];
            String buffArr[] = buff.split("-");
            Integer year = Integer.parseInt(buffArr[0]), month = Integer.parseInt(buffArr[1]), date = Integer.parseInt(buffArr[2]);
            for (CSEvent event : events) {
                Calendar tempCal = event.getDate();
                if (tempCal.get(Calendar.YEAR) == year && tempCal.get(Calendar.MONTH) == month - 1 && tempCal.get(Calendar.DATE) == date) {
                    // print event
                    System.out.println(event.elementToString());
                }
            }
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 3"));
        }
    }

    /**
     * menampilkan seluruh event dalam calendar
     *
     * @param args empty arguments
     * @throws CSWrongCommandFormatException
     */
    public void executePrintInfo(String args[]) throws CSWrongCommandFormatException {
        if (args.length == 2) {
            System.out.println(this.elementToString());
        } else {
            throw (new CSWrongCommandFormatException("invalid argument count " + args.length + ", expected 2"));
        }
    }

    /**
     * mengembalikan keadaan calendar ke n keadaan sebelumnya
     *
     * @param args
     * @throws CSWrongCommandFormatException
     */
    public void executeUndo(String[] args) throws CSWrongCommandFormatException {
        if (args.length == 2) {
            int _num = Integer.parseInt(args[1]);
            // mencari event calendar yang sesuai dengan n langkah undo
            CSCalendar otherCalendar = null;
            while (!prevCalendar.isEmpty() && _num > 0) {
                otherCalendar = prevCalendar.pop();
                afterCalendar.push(otherCalendar);
                _num--;
            }
            if (otherCalendar != null) {
                // salin isi calendar ke this calendar
                this.events = new PriorityQueue<>(otherCalendar.events.size(), new CSEvent.EventComparator());
                for (CSEvent event : otherCalendar.events) {
                    this.events.add((CSEvent) event.elementClone());
                }
                this.entryNo = otherCalendar.entryNo;
            }

        } else {
            throw new CSWrongCommandFormatException("Invalid Argument Count :" + args.length + " expected 2");
        }
    }

    /**
     * mengembalikan keadaan calendar ke n langkah selanjutnya
     *
     * @param args
     * @throws CSWrongCommandFormatException
     */
    public void executeRedo(String[] args) throws CSWrongCommandFormatException {
        if (args.length == 2) {
            int _num = Integer.parseInt(args[1]);
            // mencari calendar yang sesuai dengan n langkah redo
            CSCalendar otherCalendar = null;
            while (!afterCalendar.isEmpty() && _num > 0) {
                otherCalendar = afterCalendar.pop();
                prevCalendar.push(otherCalendar);
                _num--;
            }
            if (otherCalendar != null) {
                // salin isi calendar ke this calendar
                this.events = new PriorityQueue<>(otherCalendar.events.size(), new CSEvent.EventComparator());
                for (CSEvent event : otherCalendar.events) {
                    this.events.add((CSEvent) event.elementClone());
                }
                this.entryNo = otherCalendar.entryNo;
            }
        } else {
            throw new CSWrongCommandFormatException("Invalid Argument Count :" + args.length + " expected 2");
        }
    }

    public void onExecuted() {
        this.prevCalendar.add(new CSCalendar(this));
    }

    @Override
    public String elementToString() {
        StringBuilder buff = new StringBuilder();
        for (CSEvent event : events) {
            buff.append(event.elementToString());
        }
        return buff.toString();
    }

    @Override
    public CSCalendarElement elementClone() {
        return new CSCalendar(this);
    }

    /**
     * mengembalikan jumlah event yang ada dalam satu hari
     *
     * @param year tahun
     * @param month bulan
     * @param date tanggal
     * @return jumlah event pada hari tersebut
     */
    public int getEventCountInDay(int year, int month, int date) {
        int result = 0;
        for (CSEvent event : events) {
            Calendar tempCal = event.getDate();
            // cocokkan tanggal
            if (tempCal.get(Calendar.YEAR) == year && tempCal.get(Calendar.MONTH) == month - 1 && tempCal.get(Calendar.DATE) == date) {
                result++;
            }
        }
        return result;
    }

    @Override
    public void saveToFile(String filePath) {
        try {
            FileWriter fstream = new FileWriter(filePath);
            BufferedWriter out = new BufferedWriter(fstream);
            // write calendar entry no
            out.write(this.getEntryNo() + "\n");
            // write event count
            PriorityQueue<CSEvent> tempEvents = this.getEvents();
            out.write("" + tempEvents.size() + "\n");
            // write events : 
            for (CSEvent event : tempEvents) {
                // 1. event entry no
                out.write(event.getEntryNo() + "\n");
                // 2. description
                out.write(event.getDescription() + "\n");
                // 3. position : long, lat
                out.write(event.getPosition().getX() + "," + event.getPosition().getY() + "\n");
                // 4. date time
                Calendar eventCal = event.getDate();
                out.write(eventCal.get(Calendar.YEAR) + "-" + (eventCal.get(Calendar.MONTH) + 1) + "-" + eventCal.get(Calendar.DATE) + "@" + eventCal.get(Calendar.HOUR_OF_DAY) + ":" + eventCal.get(Calendar.MINUTE) + ":" + eventCal.get(Calendar.SECOND) + "\n");
                // 5. participant count
                ArrayList<String> eventParticipants = event.getParticipants();
                out.write(eventParticipants.size() + "\n");
                // 6. participant names
                for (String participant : eventParticipants) {
                    out.write(participant + "\n");
                }
            }
            out.close();
            fstream.close();
        } catch (IOException ex) {
            Logger.getLogger(CSCalendar.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public void loadFromFile(String filePath) {
        try {
            this.getEvents().clear();
            Integer iter, iter2, eventCount, participantCount;
            String buff;
            String buffArr[];
            // open a file to read
            Scanner in = new Scanner(new File(filePath));
            // read calendar entry no
            this.setEntryNo(Integer.parseInt(in.nextLine()));
            // read event count
            eventCount = Integer.parseInt(in.nextLine());
            // read events : 
            for (iter = 1; iter <= eventCount; iter++) {
                CSEvent tempEvent = new CSEvent();
                // 1. event entry no
                tempEvent.setEntryNo(Integer.parseInt(in.nextLine()));
                // 2. description
                tempEvent.setDescription(in.nextLine());
                // 3. position : long, lat
                buff = in.nextLine();
                buffArr = buff.split(",");
                tempEvent.setPosition(new Point2D.Float(Float.parseFloat(buffArr[0]), Float.parseFloat(buffArr[1])));
                // 4. date time
                buff = in.nextLine();
                buffArr = buff.split("@");
                String date = buffArr[0], time = buffArr[1];
                Calendar tempCal = Calendar.getInstance();
                buffArr = date.split("-");
                tempCal.set(Calendar.YEAR, Integer.parseInt(buffArr[0]));
                tempCal.set(Calendar.MONTH, Integer.parseInt(buffArr[1]) - 1);
                tempCal.set(Calendar.DATE, Integer.parseInt(buffArr[2]));
                buffArr = time.split(":");
                tempCal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(buffArr[0]));
                tempCal.set(Calendar.MINUTE, Integer.parseInt(buffArr[1]));
                tempCal.set(Calendar.SECOND, Integer.parseInt(buffArr[2]));
                tempEvent.setDate(tempCal);
                // 5. participant count
                participantCount = Integer.parseInt(in.nextLine());
                ArrayList<String> tempParticipants = new ArrayList<>();
                // 6. participant names
                for (iter2 = 1; iter2 <= participantCount; iter2++) {
                    tempParticipants.add(in.nextLine());
                }
                tempEvent.setParticipants(tempParticipants);
                this.getEvents().add(tempEvent);
            }
        } catch (FileNotFoundException ex) {
            Logger.getLogger(CSCalendar.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
